# Copyright (c) 2008, Zachary Voase
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#    1. Redistributions of source code must retain the above copyright notice, 
#       this list of conditions and the following disclaimer.
#    
#    2. Redistributions in binary form must reproduce the above copyright 
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#
#    3. Neither the name of QManager nor the names of its contributors may be
#       used to endorse or promote products derived from this software without
#       specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""
QManager is an abstracted Django manager which filters via custom queries.

Introduction
============

The ``QManager`` class is a subclass of ``django.db.models.Manager`` which can
remove some of the boilerplate in manager definitions. Managers may be used to
define 'table-level' operations on your models, and one of their most common
uses is to provide a pre-defined ``QuerySet`` for model data. The typical
programming idiom goes something like this:
    
    1.  Define your custom manager as a subclass of
        ``django.db.models.Manager``, overriding the ``get_query_set`` method
        to return a filtered query set. For example::
        
            class MyManager(models.Manager):
                def get_query_set(self):
                    return super(MyManager, self).get_query_set().filter(
                        field1='x', field2='y')
        
    2.  On your model, instantiate the manager. Following from the previous
        example::
            
            class MyModel(models.Model):
                field1 = models.CharField(...)
                field2 = models.CharField(...)
                ...
                objects = models.Manager() # Default manager instance.
                my_mgr = MyManager() # Custom manager instance.
    
Usage of ``QManager``
=====================

The problem with this pattern is that it ends up being repeated several times
for each model as you try to define several useful custom managers for each
type of data. The ``QManager`` class abstracts this idiom by providing a simple
way of defining managers to return query sets. Now, what originally involved
two parts is now down to one::
    
    class MyModel(models.Model):
        field1 = models.CharField(...)
        field2 = models.CharField(...)
        ...
        objects = models.Manager()
        my_mgr = QManager(Q(field1='x', field2='y'))

As you can see, a custom manager is defined by passing ``QManager``'s
``__init__`` method a ``Q`` object. ``Q`` objects are used in Django to specify
queries of arbitrary complexity, and by using ``QManager`` you don't need to
write all the boilerplate of the first example: all it takes is a simple ``Q``
object.

Conjunctions and Disjunctions
-----------------------------

Another feature of ``Q`` instances is that they can be combined using the ``&``
(and) and ``|`` (or) logical operators. These must not be the Python literal
``and`` and ``or`` operators, as these behave differently to the symbols. Let's
say, for example, that you have two query sets defined by ``Q`` instances whose
intersection (logical *and*) you want to find. Each of these query sets would
be defined by an instance of ``Q``, like so::
    
    query1 = Q(public=True)
    query2 = Q(confirmed=True)

Each of these return query sets which contain those records which are public
and confirmed, respectively. Such query sets would be obtained by calling the
``filter`` method on one of the model's managers (i.e. ``objects``). Suppose
you now want to find all the records which are both public *and* confirmed, in
one query set. Typically, you would define a ``Q`` instance such as
``Q(public=True, confirmed=True)``, but this violates the DRY principle, as the
same information is repeated. Instead, one may simply do this::
    
    query3 = query1 & query2

This is a shortcut which allows you to cut down the amount of extra code you
write, and maintain DRY in your code. The ``QManager`` class provides a wrapper
which lets you do the same thing in your manager definitions. An example might
be::
    
    class MyModel(models.Model):
        public = models.BooleanField()
        confirmed = models.BooleanField()
        ...
        public_objects = QManager(Q(public=True))
        confirmed_objects = QManager(Q(confirmed=True))
        public_confirmed = public_objects & confirmed_objects

Flexibility of ``QManager``
---------------------------

This use of ``&`` and ``|`` is not restricted to just ``QManager`` instances;
``Q`` objects, ``QuerySet`` instances and regular managers may all be combined
with ``QManager`` instances via the logical operators. A caveat, however: in
these statements, the ``QManager`` instance *must* come before anything else.
It is valid, for example, to do this::
    
    public_confirmed = public_objects & Q(confirmed=True)

It is an error, however, to do the following::
    
    public_confirmed = Q(confirmed=True) & public_objects

It is advisable to ensure that errors like these do not happen, as they will be
difficult to debug if they do: an error will most likely be raised somewhere in
Django's codebase, rather than for ``QManager``.

Exclusive Queries
-----------------

Another feature of query set manipulation is exclusion. ``QuerySet`` and
``Manager`` instances have ``exclude`` methods which, when called with a ``Q``
object, will return the set of all data *not* matching the conditions given in
the query. This is called finding the *complement* of a set of data, and
``QManager`` instances support this through the negation operator (``-``). For
example::
    
    class MyModel(models.Model):
        ...
        public_objects = QManager(Q(public=True))
        non_public_objects = -public_objects

If a reference to a ``QManager`` instance, whether through their definition
(i.e. ``-QManager(...)``) or a variable (i.e. ``-public_objects``), is
prepended with a ``-`` symbol, then it's ``__neg__`` magic method will be
called. In the case of ``QManager`` instances, this will return a new instance
whose query is defined as the complement of the old query (Note on
implementation: this is done via ``django.db.models.query.QNot``). This may be
used in conjunction with the other operators, for example::
    
    class MyModel(models.Model):
        ...
        public_objects = QManager(Q(public=True))
        non_public_confirmed = confirmed_objects & -public_objects
        non_confirmed_public = -confirmed_objects & public_objects

Using ``QManagerFactory`` for Refactoring ``QManager`` Definitions
------------------------------------------------------------------

A common design pattern which emerges when using ``QManager`` is the definition
of queries which apply to several similar models. For example, if several of
your models have ``public`` fields which are boolean flags telling whether or
not a record is public, then you may want a manager to get only public records.
Because, however, you are defining managers across several models, the query
``Q(public=True)`` will be repeated several times. If, one day, you decide to
change the field to being called ``is_public``, then each query will have to be
edited individually. A typical set-up might look something like this::
    
    class A(models.Model):
        public = models.BooleanField()
        ...
        public_objects = QManager(Q(public=True))
    
    class B(models.Model):
        public = models.BooleanField()
        ...
        public_objects = QManager(Q(public=True))

As you can see, because the managers are defined on different models they may
not be reused. A simple way around this is to use the ``QManagerFactory``
function in this module. At the top of your models file (or a separate file for
managers, if you wish), place the following line::
    
    PublicManager = QManagerFactory(Q(public=True), name='PublicManager')

``PublicManager`` will now be a class, with the name 'PublicManager' (due to
the ``name`` keyword), which is a subclass of ``QManager``, and has the
``query`` attribute pre-defined as ``Q(public=True)``. On each model, you now
only need to place the following::
    
    class A(models.Model):
        public = models.BooleanField()
        ...
        public_objects = PublicManager()

And likewise for ``B``. For more information on the ``QManagerFactory``
function, you are advised to consult its docstring and source code.

For more information, esp. implementation details, consult the source code. It
may also be useful to look at the official Django documentation for the ``Q``
class and database API.
"""

from copy import copy

try:
    from django.db.models import Manager
    from django.db.models.query import Q, QNot, QOperator, QuerySet
except (EnvironmentError, ImportError), exc:
    Manager, Q, QNot, QOperator, QuerySet = (object,) * 5
    print """\
Warning: Using dummy classes for Manager, Q, QNot, QOperator and QuerySet.
Nothing will work correctly; however, you can still view docstrings, and view
information on class and function names (handy when at the interpreter).
The exception raised was:
    %s: %s""" % (exc.__class__.__name__, exc.message)

class QManager(Manager):
    
    """
    Boilerplate Django manager which filters via a custom query.
    
    Introduction
    ============
    
    The ``QManager`` class is a subclass of ``django.db.models.Manager``, which
    defines several custom methods and attributes, and overrides several
    others. For more information on usage, consult the module-level
    documentation.
    
    Overridden Methods
    ==================
        
        *   ``__init__``
            Initialize a ``QManager`` instance, with an optional query.
        
        *   ``__setattr__``
            Set an attribute on a ``QManager`` to a certain value, with extras.
        
        *   ``get_query_set``
            Return the query set for a ``QManager`` instance's query.
    
    New Methods
    ===========
        
        *   ``__neg__``
            Return the complementary query of this ``QManager`` instance.
        
        *   ``__and__``
            Return the logical 'and' of a ``QManager`` with another query.
        
        *   ``__or__``
            Return the logical 'or' of a ``QManager`` with another query.
        
        *   ``__xor__``
            Return the logical 'xor' of a ``QManager`` with another query.
    
    Consult the respective documentation for a method for more information on
    that method's usage and behaviour.
    
    The ``query`` Attribute
    =======================
    
    A ``QManager`` instance has one additional attribute called ``query`` which
    holds a ``Q`` instance - the representation of a database query in Django's
    ORM. Strictly speaking, this object may be one of the following classes
    (each may be found in ``django.db.models.query``):
        
        *   ``Q``
            A simple query object, formed from zero or more lookup clauses.
        
        *   ``QNot``
            A subclass of ``Q``, this represents the logical 'not' of a query;
            in other words, the complement of the set represented by a ``Q``
            instance (or another compatible object).
        
        *   ``QAnd``
            A subclass of ``QOperator``, this represents the logical 'and'
            between two queries; in other words, the intersection of two sets
            represented by two ``Q`` instances (or other compatible objects).
        
        *   ``QOr``
            A subclass of ``QOperator``, this represents the logical 'or'
            between two queries; in other words, the union of two sets
            represented by two ``Q`` instances (or other compatible objects).
    
    For more information on query objects, consult the official documentation
    for Django's database API (this is included in the Django source).
    """
    
    def __init__(self, query=None):
        """
        Initialize a ``QManager`` instance, with an optional query.
        
        The ``__init__`` method intializes a ``QManager`` instance, giving it a
        query by trying three things (in this order):
            
            1.  Arguments passed to ``__init__`` are turned into ``Q``
                instances by the ``get_valid_query`` function in this module
                (consult docstring for more information).
                
            2.  A ``query`` attribute specified on the class itself. This may
                be present due to subclassing (see docstring for the
                ``QManagerFactory()`` function in this module).
                
            3.  An empty ``Q`` instance (given by initializing ``Q`` with no
                arguments).
            
        It then calls the ``Manager`` class's default ``__init__`` method on
        the instance.
        """
        if query:
            self.query = get_valid_query(query)
        elif hasattr(self, 'query'):
            self.query = get_valid_query(self.query)
        else:
            self.query = Q()
        super(QManager, self).__init__()
    
    def __neg__(self):
        """
        Return the complementary query of this ``QManager`` instance.
        
        ``__neg__`` provides a shortcut for finding the complement of a query
        set by returning a new ``QManager`` instance in which ``QNot`` has been
        initialized on the query. Filtering by a ``QNot`` instance (a subclass
        of ``Q``) is the same as excluding by the original ``Q``, thus allowing
        ``QManager`` instances to be complemented.
        
        By defining this as ``__neg__``, it allows the shortcut of
        ``-manager_name`` to be used instead of calling, for example,
        ``manager_name.not_()``.
        """
        return self.__class__(QNot(self.query))
    
    def __and__(self, query):
        """
        Return the logical 'and' of this ``QManager`` with another query.
        
        ``__and__`` allows the combination of this instance's query with
        another query via the 'and' logical operator. ``Q`` objects support
        this inherently, so all that needs to be done is for a new ``QManager``
        instance to be created using the ``&`` of the instance's query and the
        argument query.
        
        In order to allow objects of all sorts to be and'd onto a ``QManager``
        instance, ``get_valid_query`` is called on the argument. This does not
        modify the original argument.
        """
        return self.__class__(self.query & get_valid_query(query))
    
    def __or__(self, query):
        """
        Return the logical 'or' of this ``QManager`` with another query.
        
        ``__or__`` allows the combination of this instance's query with
        another query via the 'or' logical operator. ``Q`` objects support this
        inherently, so all that needs to be done is for a new ``QManager``
        instance to be created using the ``|`` of the instance's query and the
        argument query.
        
        In order to allow objects of all sorts to be or'd onto a ``QManager``
        instance, ``get_valid_query`` is called on the argument. This does not
        modify the original argument.
        """
        return self.__class__(self.query | get_valid_query(query))
    
    def __xor__(self, query):
        """
        Return the logical 'xor' of this ``QManager`` with another query.
        
        ``__xor__`` allows the combination of this instance's query with
        another query via the 'xor' logical operator. ``Q`` objects support
        this inherently, so all that needs to be done is for a new ``QManager``
        instance to be created using the ``^`` of the instance's query and the
        argument query.
        
        Definition and Implementation
        =============================
        
        The definition of an XOR is that it returns the records which fit
        *either* one query or the other - but not *both*. In terms of set
        theory (with A and B representing the sets returned by both queries),
        the XOR of A and B is the intersection of the union of the complements
        of A with B, and the union of A with B. In terms of boolean logic, XOR
        is defined as the AND of the OR of NOT A with NOT B, and the OR of A
        and B. In Python code::
            
            def xor(a, b):
                return ((not a) or (not b)) and (a or b)
        
        Consult the source code for more information on the implementation.
        
        In order to allow objects of all sorts to be xor'd onto a ``QManager``
        instance, ``get_valid_query`` is called on the argument. This does not
        modify the original argument.
        """
        query = get_valid_query(query)
        return self.__class__(
            (QNot(self.query) | QNot(query)) &
            (self.query | query))
    
    def __setattr__(self, attr, value):
        """
        Set an attribute on a ``QManager`` to a certain value, with extras.
        
        This does not differ greatly from the regular ``__setattr__``, except
        that, if the ``query`` attribute is being modified, then
        ``get_valid_query`` is called on the value which it is being set to, to
        allow queries to be validated and to come from several different
        sources.
        
        As always, it is a good idea to consult the source code, as well as the
        source and docstring for the ``get_valid_query`` function.
        """
        if attr == 'query':
            value = get_valid_query(copy(value))
        return super(QManager, self).__setattr__(attr, value)
    
    def get_query_set(self):
        """
        Return the query set for this ``QManager`` instance's query.
        
        The ``QManager`` class's ``get_query_set`` method is a simple line of
        code::
        
            return super(QManager, self).get_query_set().filter(self.query)
        
        What it essentially does is returns the basic ``Manager`` class's query
        set for a model (i.e. all of a model's records), filtered by the
        ``QManager`` instance's query. This forms the main part of the
        abstraction which ``QManager`` provides.
        """
        return super(QManager, self).get_query_set().filter(self.query)


def QManagerFactory(query, name=None):
    """
    Construct a ``QManager`` subclass with a pre-defined query.
    
    Introduction
    ============
    
    The ``QManagerFactory`` function returns a new class, subclassed from
    ``QManager``, which has its ``query`` attribute defined at a class-level,
    rather than having to be specified on each instance. This allows the new
    class to be instantiated, with no arguments, and return a ``QManager`` with
    the prescribed query. For more information on how it does this, consult the
    docstring and source for ``QManager.__init__``.
    
    A ``name`` keyword argument may also be specified, which will be used as
    the name of the returned class (by setting the class's ``__name__``
    attribute to this value).
    
    Usage
    =====
    
    This function is useful in cases where a query is needed across several
    model definitions, such as when several slightly different models share a
    common field, or set of fields, which would benefit from having some
    common, pre-defined queries.
    
    An example of this may include having several models which each have images
    associated with them via ManyToOne relationships on other models. This may
    look something like this (showing only the image classes)::
        
        ...
        class AImage(models.Model):
            a = models.ForeignKey(A)
            image = models.ImageField(...)
            public = models.BooleanField()
        
        class BImage(models.Model):
            b = models.ForeignKey(B)
            image = models.ImageField(...)
            public = models.BooleanField()
    
    As you can see, each model has a ``public`` boolean field. You may wish to
    specify managers for the public and non-public images for each. Using the
    ``QManager`` class, this would be done on each like so::
        
        class AImage(models.Model):
            ...
            public_images = QManager(Q(public=True))
            non_public_images = -public_images
        
        class BImage(models.Model):
            ...
            public_images = QManager(Q(public=True))
            non_public_images = -public_images
    
    As you can see, the query ``Q(public=True)`` is repeated on each model
    definition. This violates the DRY principle, and so a solution is required
    if we are to keep code manageable. By using ``QManagerFactory``, this
    problem is solved. Somewhere in your models file (or elsewhere), the
    following should be added::
        
        PublicImageManager = QManagerFactory(
            Q(public=True), name='PublicImageManager')
    
    Then, on each model, the ``QManager`` definitions may be replaced by this::
        
        class AImage(models.Model):
            ...
            public_images = PublicImageManager()
            non_public_images = -public_images
        
        class BImage(models.Model):
            ...
            public_images = PublicImageManager()
            non_public_images = -public_images
        
    By doing this, code adheres to DRY, and life is made easier for you, the
    developer, by saving space and time. For more information on how subclasses
    are constructed, consult this function's source code.
    """
    class QManagerSubclass(QManager):
        query = query
    if name is not None:
        QManagerSubclass.__name__ = name
    return QManagerSubclass


def get_query(obj):
    """
    Given an object, return (somehow) a ``Q`` instance.
    
    Usage and Implementation
    ========================
    
    ``get_query`` accepts an object, and runs it through several tests to try
    and retrieve a query object. These tests are (in this order)::
        
        1.  If the object is an instance of ``QManager``, return its ``query``
            attribute.
        
        2.  If the object is an instance of ``django.db.models.Manager``,
            get its query set, and return the ``_filters`` attribute on the
            ``QuerySet`` instance.
        
        3.  If the object is a ``QuerySet`` instance, return its ``_filters``
            attribute.
    
    If none of these tests succeed, the object is returned as-is. This
    indicates either that the object is already a ``Q`` instance, or that it is
    of an unknown type.
    
    This function does not validate that objects returned are, in fact, ``Q``
    instances. For validation, see the ``get_valid_query`` function.
    """
    if isinstance(obj, QManager):
        return obj.query
    elif isinstance(obj, Manager):
        return obj.get_query_set()._filters
    elif isinstance(obj, QuerySet):
        return obj._filters
    return obj


def get_valid_query(query):
    """
    Given an object, return a ``Q`` instance, or raise an error.
    
    Introduction
    ============
    
    ``get_valid_query`` works by calling ``get_query`` on the provided object.
    The returned object is then examined using ``isinstance``. If it is not an
    instance of either ``django.db.models.query.Q`` or
    ``django.db.models.query.QOperator``, then a ``TypeError`` is raised.
    Otherwise, the object is returned.
    
    Testing for ``Q`` or ``QOperator`` encompasses all possible types of query.
    Queries which have been and'd/or'd together will be instances of ``QAnd``
    or ``QOr`` respectively. Both of these are subclasses of ``QOperator``, and
    so will test as valid. ``QNot`` (a complemented query) is a subclass of
    ``Q``, and so will pass as well. ``Q`` instances will, obviously, pass.
    
    By returning the query, it allows this function to be used wherever a valid
    query is required, eliminating a small amount of boilerplate from the
    definition from ``QManager``.
    """
    q = get_query(query)
    if not isinstance(q, (Q, QOperator)):
        raise TypeError('Not a query object: %r' % (query,))
    return q
